Skip to content
Pasqal Documentation

Tutorial 5 - Advanced quantum programming

Note: This tutorial is designed for users who already understand quantum programming using the pulser library. For more details on this library, see its tutorial (external).

By design, a quantum solver is composed of two main quantum components for solving a problem:

  • an embedder, i.e. a mechanism used to customize the layout of neutral atoms on the quantum device.
  • a pulse shaper, i.e. a mechanism used to customize the laser pulse to which the neutral atoms are subjected during the execution of the quantum algorithm.

These can be specified in the SolverConfig. Most users can use the default embedders and pulse shapers provided with this library. However, if you wish to alter the behavior of the solver, perhaps for the sake of research, debugging or learning, you can implement your own embedders or pulse shapers. This tutorial dives into currently available components, as well as designing custom ones.

When instantiating a SolverConfig, a default embedder is already made available. To access the resulting embedding from a solver, simply call the embedding method as follows:

from mis import MISSolver, MISInstance, SolverConfig, BackendConfig, BackendType
from networkx import erdos_renyi_graph
# User can fix the seed for reproducibility
seed = 0
graph = erdos_renyi_graph(n=25, p=0.4, seed=seed)
instance = MISInstance(graph)
qutip_config = BackendConfig(
backend = BackendType.QUTIP
)
config = SolverConfig(backend = qutip_config)
solver = MISSolver(instance, config)
geometry = solver.embedding()
# draw the register
# geometry.draw()

To design our own embedding method, we need to define a class inhereting from mis.pipeline.embedder.BaseEmbedder and implement an embed method as follows:

from mis.pipeline.embedder import BaseEmbedder
from pulser import Register
from qoolqit._solvers.backends import BaseBackend
from mis.pipeline.layout import Layout
class DefaultEmbedder(BaseEmbedder):
""" The DefaultEmbedder class available in mis.
"""
def embed(self, instance: MISInstance, config: SolverConfig, backend: BaseBackend) -> Register:
device = backend.device()
# Use Layout helper to get rescaled coordinates and interaction graph
layout = Layout.from_device(data=instance, device=device)
# Finally, prepare register.
return Register(
qubits={f"q{node}": pos for (node, pos) in layout.coords.items()}
)
custom_config = SolverConfig(backend = qutip_config, embedder=DefaultEmbedder())
custom_solver = MISSolver(instance, custom_config)
default_geometry = custom_solver.embedding()
# draw the register
# default_geometry.draw()

When instantiating a SolverConfig, a default pulse shaper is already made available. To access the resulting embedding from a solver, simply call the pulse method as follows:

default_pulse = solver.pulse(solver.embedding())
default_pulse

To design our own pulse shaping method, we need to define a class inhereting from mis.pipeline.pulse.BasePulseShaper and implement a pulse method and a detuning method as follows:

from mis.pipeline.pulse import BasePulseShaper
from pulser import Pulse
from qoolqit._solvers import Detuning
class ConstantPulseShaper(BasePulseShaper):
"""
We simply return a constant pulse.
"""
def pulse(
self, config: SolverConfig, register: Register, backend: BaseBackend, instance: MISInstance
) -> Pulse:
import numpy as np
return Pulse.ConstantPulse(1000, np.pi, 0, 0)
def detuning(
self, config: SolverConfig, register: Register, backend: BaseBackend, instance: MISInstance
) -> list[Detuning]:
return list()
constant_pulse_config = SolverConfig(backend = qutip_config, pulse_shaper=ConstantPulseShaper())
constant_pulse_solver = MISSolver(instance, constant_pulse_config)
constant_pulse = constant_pulse_solver.pulse(constant_pulse_solver.embedding())
constant_pulse